/**
 * This file is part of 1genia trampoline
 * Copyright (C) 2007 1genia (contact@1genia.com)
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; version 3 of the License. 
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details. 
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; see the file COPYING.TXT.  If not,
 * write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA 02110-1301, USA. 
 **/
package com.genia.toolbox.basics.process;

import java.io.Serializable;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.util.ObjectUtils;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.Factory;

import com.genia.toolbox.basics.bean.Pair;
import com.genia.toolbox.basics.bean.Transformer;
import com.genia.toolbox.basics.bean.impl.PairImpl;
import com.genia.toolbox.basics.bean.impl.SafeHashMapMethodInterceptor;
import com.genia.toolbox.basics.proxy.methods.interceptors.NoOpMethodIntercetor;
import com.genia.toolbox.persistence.bean.CrossSessionObject;

import javassist.util.proxy.ProxyObject;

/**
 * Static class helper to compare classes when there is proxies.
 */
public class ProxyHelper
{

  /**
   * the {@link Set} of classes that are used by proxies.
   */
  private static final Set<String> CLASSES_TO_IGNORE = new HashSet<String>(Arrays.asList(CrossSessionObject.class.getName(), Serializable.class.getName(), Factory.class.getName(), Proxy.class
      .getName(), net.sf.cglib.proxy.Proxy.class.getName(), ProxyObject.class.getName(), "net.sf.cglib.proxy.Proxy$ProxyImpl", "org.hibernate.proxy.HibernateProxy"));

  /**
   * tag for the class instrumented through cglib.
   */
  private static final String ENHANCER_BY_CGLIB_CLASSNAME = "$$EnhancerByCGLIB$$";

  /**
   * tag for the class instrumented through cglib.
   */
  private static final String ENHANCER_BY_JAVASSIST_CLASSNAME = "_javassist_";

  /**
   * the {@link Map} containing all the already created proxies.
   */
  private static final Map<Pair<Class<?>, ? extends Set<Class<?>>>, Factory> FACTORIES = new ConcurrentHashMap<Pair<Class<?>, ? extends Set<Class<?>>>, Factory>();



  /**
   * returns a {@link Factory} to create a proxy depending on the base class and
   * the implemented interfaces.
   * 
   * @param baseClass
   *          the base class of the proxy
   * @param interfaces
   *          the interfaces implemented by the proxy
   * @return a {@link Factory} to create a proxy depending on the base class and
   *         the implemented interfaces
   */
  public static Factory getProxyFactory(Class<?> baseClass, Class<?>... interfaces)
  {
    final Class<?> unproxiedClass = ProxyHelper.unProxy(baseClass);
    Pair<Class<?>, ? extends Set<Class<?>>> proxyDescription = new PairImpl<Class<?>, Set<Class<?>>>(unproxiedClass, new HashSet<Class<?>>(Arrays.asList(interfaces)));
    // Double-Checked Locking is usable because FACTORIES is a synchronized map.
    Factory factory = FACTORIES.get(proxyDescription);
    if (factory == null) {
      synchronized (ProxyHelper.class) {
        factory = FACTORIES.get(proxyDescription);
        if (factory == null) {
          Enhancer enhancer = new Enhancer();
          enhancer.setSuperclass(unproxiedClass);
          enhancer.setInterfaces(interfaces.length == 0 ? null : interfaces);
          enhancer.setCallback(NoOpMethodIntercetor.INSTANCE);
          factory = (Factory) enhancer.create();
          FACTORIES.put(proxyDescription, factory);
        }
      }
    }
    return factory;
  }



  /**
   * returns a {@link Map} that never returns <code>null</code> elements. If
   * possible use {@link ProxyHelper#getSafeMap(ConcurrentHashMap, Transformer)}
   * as it is more efficient than this method.
   * 
   * @param <K>
   *          the type of the keys of the map
   * @param <V>
   *          the type of the values of the map
   * @param <M>
   *          the type of the map
   * @param originalMap
   *          the original map
   * @param tranformer
   *          the object that allows to create entity for missing elements
   * @return a {@link Map} that never returns <code>null</code> elements
   */
  @SuppressWarnings("unchecked")
  public static <K, V, M extends Map<K, V>> M getSafeMap(M originalMap, Transformer<K, V> tranformer)
  {
    Factory factory = getProxyFactory(originalMap.getClass());
    return (M) factory.newInstance(new SafeHashMapMethodInterceptor(originalMap, tranformer, true));
  }



  /**
   * returns a {@link Map} that never returns <code>null</code> elements.
   * 
   * @param <K>
   *          the type of the keys of the map
   * @param <V>
   *          the type of the values of the map
   * @param <M>
   *          the type of the map
   * @param originalMap
   *          the original map
   * @param tranformer
   *          the object that allows to create entity for missing elements
   * @return a {@link Map} that never returns <code>null</code> elements
   */
  @SuppressWarnings("unchecked")
  public static <K, V, M extends ConcurrentHashMap<K, V>> M getSafeMap(M originalMap, Transformer<K, V> tranformer)
  {
    Factory factory = getProxyFactory(originalMap.getClass());
    return (M) factory.newInstance(new SafeHashMapMethodInterceptor(originalMap, tranformer, false));
  }



  /**
   * compare 2 classes. It returns <code>true</code> if the two classes, once
   * stripped of any proxy are equals.
   * 
   * @param class1
   *          the first {@link Class} to compare
   * @param class2
   *          the second {@link Class} to compare
   * @return <code>true</code> if and only if the 2 classes are equals once
   *         stripped of their proxies
   */
  public static boolean compareClasses(Class<?> class1, Class<?> class2)
  {
    if (ObjectUtils.nullSafeEquals(class1, class2)) {
      return true;
    }
    if (class1 == null || class2 == null) {
      return false;
    }
    if (!isProxy(class1) && !isProxy(class2)) {
      return false;
    }
    if (getFirstRealParent(class1) != getFirstRealParent(class2)) {
      return false;
    }
    return getRealClasses(class1).equals(getRealClasses(class2));
  }



  /**
   * returns the first super class of a class that is not a Proxy.
   * 
   * @param classz
   *          the {@link Class} to examine
   * @return the first super class of a class that is not a Proxy
   */
  private static Class<?> getFirstRealParent(Class<?> classz)
  {
    if (classz == null || classz.isInterface() || ignoreClass(classz)) {
      return null;
    }
    if (!isProxy(classz)) {
      return classz;
    }
    return getFirstRealParent(classz.getSuperclass());
  }



  /**
   * returns the {@link Set} of non proxy classes that a {@link Class} is
   * composed.
   * 
   * @param classz
   *          the class to examine
   * @return the {@link Set} of non proxy classes that a {@link Class} is
   *         composed
   */
  private static Set<Class<?>> getRealClasses(Class<?> classz)
  {
    HashSet<Class<?>> res = new HashSet<Class<?>>();
    if (classz == null || ignoreClass(classz)) {
      return res;
    }
    if (!isProxy(classz)) {
      res.add(classz);
    }
    res.addAll(getRealClasses(classz.getSuperclass()));
    for (Class<?> implementedInterface : classz.getInterfaces()) {
      res.addAll(getRealClasses(implementedInterface));
    }
    return res;
  }



  /**
   * returns whether a class must be considered as a proxy implementation.
   * 
   * @param classz
   *          the class to consider
   * @return whether a class must be considered as a proxy implementation
   */
  private static boolean ignoreClass(Class<?> classz)
  {
    return CLASSES_TO_IGNORE.contains(classz.getName());
  }



  /**
   * returns whether a {@link Class} is a proxy.
   * 
   * @param class1
   *          the {@link Class} to examine
   * @return <code>true</code> if and only if the {@link Class} if a proxy
   */
  public static boolean isProxy(Class<?> class1)
  {
    if (class1 == null || class1.isInterface() || Object.class == class1) {
      return false;
    }
    if (Proxy.isProxyClass(class1) || net.sf.cglib.proxy.Proxy.isProxyClass(class1)) {
      return true;
    }
    return class1.getName().contains(ENHANCER_BY_CGLIB_CLASSNAME) || class1.getName().contains(ENHANCER_BY_JAVASSIST_CLASSNAME);
  }



  /**
   * returns the first non proxy class on the class hiearchy.
   * 
   * @param class1
   *          the class to un-proxy
   * @return the first non proxy class on the class hiearchy
   */
  public static Class<?> unProxy(Class<?> class1)
  {
    if (isProxy(class1)) {
      return unProxy(class1.getSuperclass());
    }
    return class1;
  }



  /**
   * private constructor for static class.
   */
  private ProxyHelper()
  {
    // Do nothing
  }
}
